package com.example.alex_zhang.myapplication;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

public class GifImageDecoder {
    private static final String TAG = GifImageDecoder.class.getSimpleName();
    private final GifImageDecoder self = this;

    // File read status: No errors.
    public static final int STATUS_OK = 0;
    // File read status: Error decoding file (may be partially decoded)
    public static final int STATUS_FORMAT_ERROR = 1;
    // File read status: Unable to open source.
    public static final int STATUS_OPEN_ERROR = 2;
    // Trailer
    private static final byte TRR_CODE = (byte) 0x3B;
    // Image Block
    private static final byte IMG_CODE = (byte) 0x2C;
    // Extension
    private static final byte EXT_CODE = (byte) 0x21;
    // Graphic Control Extension
    private static final byte GC_EXT = (byte) 0xF9;
    // Application Extension
    private static final byte APP_EXT = (byte) 0xFF;
    // Comment Extension
    private static final byte CMT_EXT = (byte) 0xFE;
    // Plain Text Extension
    private static final byte TXT_EXT = (byte) 0x01;

    private static final int MIN_DELAY = 100;
    private static final int MIN_DELAY_ENFORCE_THRESHOLD = 20;

    protected int mStatus;
    protected int mWidth; // full mCurrentImage mWidth
    protected int mHeight; // full mCurrentImage mHeight
    protected Bitmap mCurrentImage; // current frame
    protected Bitmap mLastImage; // previous frame
    protected int mDispose = 0; // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
    protected int mLastDispose = 0;
    protected int mDelay = 0; // mDelay in milliseconds
    protected ArrayList<GifFrame> mGifFrames; // mGifFrames read from current file
    protected int mFrameCount;

    private int mOffset = 0;

    private GifHeader mGifHeader;
    private GraphicControlExtension mGcExt;
    private ImageBlock mImageBlock;

    private static class GifFrame {
        public GifFrame(Bitmap im, int del) {
            image = im;
            delay = del;
        }

        public Bitmap image;
        public int delay;
    }

    /**
     * Gets display duration for specified frame.
     *
     * @param n int index of frame
     * @return delay in milliseconds
     */
    public int getDelay(int n) {
        mDelay = -1;
        if ((n >= 0) && (n < mFrameCount)) {
            mDelay = mGifFrames.get(n).delay;
            if (mDelay < MIN_DELAY_ENFORCE_THRESHOLD) {
                mDelay = MIN_DELAY;
            }
        }
        return mDelay;
    }

    /**
     * Gets the number of GifFrames read from file.
     *
     * @return frame count
     */
    public int getFrameCount() {
        return mFrameCount;
    }

    /**
     * Gets the first (or only) image read.
     *
     * @return BufferedBitmap containing first frame, or null if none.
     */
    public Bitmap getBitmap() {
        return getFrame(0);
    }

    /**
     * Gets the image contents of frame n.
     *
     * @return BufferedBitmap representation of frame, or null if n is invalid.
     */
    public Bitmap getFrame(int n) {
        if (mFrameCount <= 0)
            return null;
        n = n % mFrameCount;
        return (mGifFrames.get(n)).image;
    }

    /**
     * Reads GIF image from stream
     *
     * @param is containing GIF file.
     * @return read status code (0 = no errors)
     */

    public int read(InputStream is) throws IOException {
        init();
        if (is != null) {
            byte[] buffer = streamToBytes(is);
            mGifHeader = new GifHeader(buffer, mOffset);
            mOffset += mGifHeader.size;
            mWidth = mGifHeader.getWidth();
            mHeight = mGifHeader.getHeight();
            if (!mGifHeader.getSignature().equals("GIF")) {
                return STATUS_FORMAT_ERROR;
            }

            while (buffer[mOffset] != TRR_CODE) {
                if (buffer[mOffset] == IMG_CODE) {
                    // ImageBlock
                    mImageBlock = new ImageBlock(buffer, mOffset);
                    mOffset += mImageBlock.size;

                    mFrameCount++;
                    // create new image to receive frame data
                    mCurrentImage = extractImage();
                    if (mLastDispose > 0) {
                        if (mLastDispose == 3) {
                            // use image before last
                            int n = mFrameCount - 2;
                            if (n > 0) {
                                mLastImage = getFrame(n - 1);
                            } else {
                                mLastImage = null;
                            }
                        }
                    }
                    mGifFrames.add(new GifFrame(mCurrentImage, mDelay)); // add image to frame
                    resetFrame();
                } else if (buffer[mOffset] == EXT_CODE) {
                    if (buffer[mOffset + 1] == GC_EXT) {
                        //GraphicControlExtension
                        mGcExt = new GraphicControlExtension(buffer, mOffset);
                        mOffset += mGcExt.size;
                        mDispose = mGcExt.getDisposalMothod(); // disposal method
                        if (mDispose == 0) {
                            mDispose = 1; // elect to keep old image if discretionary
                        }
                        mDelay = mGcExt.getDelayTime() * 10; // delay in milliseconds
                    } else if (buffer[mOffset + 1] == APP_EXT) {
                        //ApplicationExtension
                        ApplicationExtension appExt = new ApplicationExtension(buffer, mOffset);
                        mOffset += appExt.size;
                    } else if (buffer[mOffset + 1] == CMT_EXT) {
                        //CommentExtension
                        CommentExtension cmtExt = new CommentExtension(buffer, mOffset);
                        mOffset += cmtExt.size;
                    } else if (buffer[mOffset + 1] == TXT_EXT) {
                        //PlainTextExtension
                        PlainTextExtension txtExt = new PlainTextExtension(buffer, mOffset);
                        mOffset += txtExt.size;
                    } else {
                        throw new IOException();
                    }
                } else {
                    throw new IOException();
                }
            }
        } else {
            mStatus = STATUS_OPEN_ERROR;
        }
        return mStatus;
    }

    public Bitmap readFirstFrame(InputStream is) throws IOException {
        mStatus = STATUS_OK;
        mFrameCount = 0;
        if (is != null) {
            byte[] bytes = streamToBytes(is);
            mGifHeader = new GifHeader(bytes, mOffset);
            mOffset += mGifHeader.size;
            mWidth = mGifHeader.getWidth();
            mHeight = mGifHeader.getHeight();

            if (!mGifHeader.getSignature().equals("GIF")) {
                throw new IllegalStateException("");
            }
            while (bytes[mOffset] != TRR_CODE) {
                if (bytes[mOffset] == IMG_CODE) {
                    mImageBlock = new ImageBlock(bytes, mOffset);
                    mOffset += mImageBlock.size;
                    return extractImage();

                } else if (bytes[mOffset] == EXT_CODE) {
                    if (bytes[mOffset + 1] == GC_EXT) {
                        //GraphicControlExtension
                        mGcExt = new GraphicControlExtension(bytes, mOffset);
                        mOffset += mGcExt.size;
                        mDispose = mGcExt.getDisposalMothod(); // disposal method
                        if (mDispose == 0) {
                            mDispose = 1; // elect to keep old image if discretionary
                        }
                        mDelay = mGcExt.getDelayTime() * 10; // delay in milliseconds
                    } else if (bytes[mOffset + 1] == APP_EXT) {
                        //ApplicationExtension
                        ApplicationExtension appExt = new ApplicationExtension(bytes, mOffset);
                        mOffset += appExt.size;
                    } else if (bytes[mOffset + 1] == CMT_EXT) {
                        //CommentExtension
                        CommentExtension cmtExt = new CommentExtension(bytes, mOffset);
                        mOffset += cmtExt.size;
                    } else if (bytes[mOffset + 1] == TXT_EXT) {
                        //PlainTextExtension
                        PlainTextExtension txtExt = new PlainTextExtension(bytes, mOffset);
                        mOffset += txtExt.size;
                    } else {
                        throw new IOException();
                    }
                } else {
                    throw new IOException();
                }
            }
        }
        return null;
    }

    /**
     * Initializes or re-initializes reader
     */
    protected void init() {
        mStatus = STATUS_OK;
        mFrameCount = 0;
        mGifFrames = new ArrayList<GifFrame>();
    }

    /**
     * Resets frame state for reading next image.
     */
    protected void resetFrame() {
        mLastDispose = mDispose;
        mLastImage = mCurrentImage;
        mDispose = 0;
        mDelay = 0;
    }

    /**
     * Extract new image
     *
     * @return image
     */
    @SuppressLint("NewApi")
    private Bitmap extractImage() {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            outputStream.write(mGifHeader.bytes);
            if (mGcExt != null) {
                if ((mWidth != mImageBlock.getImageWidth() || mHeight != mImageBlock.getImageHeight()) &&
                        mGcExt.getTransparentColorFlag() == 0) {
                    mGcExt.setTransparentColorFlagTrue();
                }
                outputStream.write(mGcExt.bytes);
            }
            outputStream.write(mImageBlock.bytes);
            outputStream.write((byte) 0x3B);
            outputStream.flush();

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inMutable = true;
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            Bitmap newBitmap = BitmapFactory.decodeStream(new BufferedInputStream(new ByteArrayInputStream(outputStream.toByteArray())));
            if (newBitmap != null) {
                if (mLastImage == null) {
                    return newBitmap;
                } else {
                    Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.RGB_565);
                    Canvas canvas = new Canvas(bitmap);
                    canvas.drawBitmap(mLastImage, 0, 0, null);
                    canvas.drawBitmap(newBitmap, 0, 0, null);
                    return bitmap;
                }
            } else {
                if (mLastImage != null) {
                    return mLastImage;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return null;
    }

    private class GifHeader {
        public byte[] bytes;
        public int size;

        public GifHeader(byte[] bytes, int offset) {
            boolean globalColorTableFlag = (bytes[offset + 0x0A] & 0x80) != 0x00;
            int globalColorTableSize = (bytes[offset + 0x0A] & 0x07);

            // get size
            size = 0x0D;
            if (globalColorTableFlag) {
                size += Math.pow(2, (globalColorTableSize + 1)) * 3;
            }

            this.bytes = new byte[size];
            System.arraycopy(bytes, offset, this.bytes, 0, size);
        }

        public String getSignature() {
            return new String(bytes, 0, 3);
        }

        public String getVersion() {
            return new String(bytes, 3, 3);
        }

        public int getWidth() {
            return (bytes[6] & 0xFF) + ((bytes[7] & 0xFF) << 8);
        }

        public int getHeight() {
            return (bytes[8] & 0xFF) + ((bytes[9] & 0xFF) << 8);
        }

        public int getGlobalColorTableFlag() {
            return (bytes[10] & 0x80) >> 7;
        }

        public int getColorResolution() {
            return (bytes[10] & 0x70) >> 4;
        }

        public int getSortFlag() {
            return (bytes[10] & 0x08) >> 3;
        }

        public int getSizeOfGlobalColorTable() {
            return (bytes[10] & 0x07);
        }

        public int getBackgroundColorIndex() {
            return bytes[11] & 0xFF;
        }

        public int getPixelAspectRatio() {
            return bytes[12];
        }

        public int[] getGlobalColorTable() {
            if (getGlobalColorTableFlag() == 0) {
                return new int[0];
            }
            int[] colors = new int[(int) Math.pow(2, getSizeOfGlobalColorTable() + 1)];
            for (int i = 0; i < colors.length; i++) {
                colors[i] = ((bytes[13 + (i * 3)] & 0xFF) << 16) + ((bytes[13 + (i * 3) + 1] & 0xFF) << 8) + (bytes[13 + (i * 3) + 2] & 0xFF);
            }
            return colors;
        }
    }

    private class ImageBlock {
        public byte[] bytes;
        public int size;

        public ImageBlock(byte[] bytes, int offset) {
            int blockSize;
            boolean localColorTableFlag = (bytes[offset + 0x09] & 0x80) != 0x00;
            int localColorTableSize = (bytes[offset + 0x09] & 0x07);

            //get size
            size = 0x0A;
            if (localColorTableFlag) {
                size += Math.pow(2, (localColorTableSize + 1)) * 3;
            }
            size += 1; //LZW Minimum Code Size

            //ImageData
            blockSize = bytes[offset + size] & 0xFF;
            size += 1;
            while (blockSize != 0x00) {
                size += blockSize;
                blockSize = bytes[offset + size] & 0xFF;
                size += 1;
            }

            this.bytes = new byte[size];
            System.arraycopy(bytes, offset, this.bytes, 0, size);
        }

        public int getImageSeparator() {
            return bytes[0] & 0xFF;
        }

        public int ImageLeftPosition() {
            return (bytes[1] & 0xFF) + ((bytes[2] & 0xFF) << 8);
        }

        public int getImageTopPosition() {
            return (bytes[3] & 0xFF) + ((bytes[4] & 0xFF) << 8);
        }

        public int getImageWidth() {
            return (bytes[5] & 0xFF) + ((bytes[6] & 0xFF) << 8);
        }

        public int getImageHeight() {
            return (bytes[7] & 0xFF) + ((bytes[8] & 0xFF) << 8);
        }

        public int getLocalColorTableFlag() {
            return (bytes[9] & 0x80) >> 7;
        }

        public int getInterlaceFlag() {
            return (bytes[9] & 0x40) >> 6;
        }

        public int getSortFlag() {
            return (bytes[9] & 0x20) >> 5;
        }

        public int getReserved() {
            return (bytes[9] & 0x18) >> 2;
        }

        public int getSizeOfLocalColorTable() {
            return bytes[9] & 0x03;
        }

        public int[] getLocalColorTable() {
            if (getLocalColorTableFlag() == 0) {
                return new int[0];
            }
            int[] colors = new int[(int) Math.pow(2, getSizeOfLocalColorTable() + 1)];
            for (int i = 0; i < colors.length; i++) {
                colors[i] = ((bytes[10 + (i * 3)] & 0xFF) << 16) + ((bytes[10 + (i * 3) + 1] & 0xFF) << 8) + (bytes[10 + (i * 3) + 2] & 0xFF);
            }
            return colors;
        }

        public int getLZWMinimumCodeSize() {
            if (getLocalColorTableFlag() == 0) {
                return bytes[10] & 0xFF;
            } else {
                return bytes[10 + (int) Math.pow(2, getSizeOfLocalColorTable() + 1) * 3] & 0xFF;
            }
        }
    }

    private class ApplicationExtension {
        public byte[] bytes;
        public int size;

        public ApplicationExtension(byte[] bytes, int offset) {
            int blockSize;
            // get size
            size = 0x0E;

            blockSize = bytes[offset + size] & 0xFF;
            size += 1;
            while (blockSize != 0x00) {
                size += blockSize;
                blockSize = bytes[offset + size] & 0xFF;
                size += 1;
            }

            this.bytes = new byte[size];
            System.arraycopy(bytes, offset, this.bytes, 0, size);
        }

        public int getExtensionIntroducer() {
            return bytes[0] & 0xFF;
        }

        public int getExtensionLabel() {
            return bytes[1] & 0xFF;
        }

        public int getBlockSize1() {
            return bytes[2] & 0xFF;
        }

        public String getApplicationIdentifier() {
            return new String(bytes, 3, 8);
        }

        public String getApplicationAuthenticationCode() {
            return new String(bytes, 11, 3);
        }
    }

    private class GraphicControlExtension {
        public byte[] bytes;
        public int size;

        public GraphicControlExtension(byte[] bytes, int offset) {
            size = 8;
            this.bytes = new byte[size];
            System.arraycopy(bytes, offset, this.bytes, 0, size);
        }

        public int getExtensionIntroducer() {
            return bytes[0] & 0xFF;
        }

        public int getGraphicControlLabel() {
            return bytes[1] & 0xFF;
        }

        public int getBlockSize() {
            return bytes[2] & 0xFF;
        }

        public int getReserved() {
            return (bytes[3] & 0xE0) >> 5;
        }

        public int getDisposalMothod() {
            return (bytes[3] & 0x1C) >> 2;
        }

        public int getUserInputFlag() {
            return (bytes[3] & 0x02) >> 1;
        }

        public int getTransparentColorFlag() {
            return (bytes[3] & 0x01);
        }

        public int getDelayTime() {
            return (bytes[4] & 0xFF) + ((bytes[5] & 0xFF) << 8);
        }

        public int getTransparentColorIndex() {
            return bytes[6];
        }

        public void setTransparentColorFlagTrue() {
            int value = getReserved() | getDisposalMothod() | getUserInputFlag() | 0x01;
            bytes[3] = (byte) Integer.parseInt(toHex(value, 2), 16);
        }
    }

    private class CommentExtension {
        public byte[] bytes;
        public int size;

        public CommentExtension(byte[] bytes, int offset) {
            int blockSize;
            // get size
            size = 0x02;

            blockSize = bytes[offset + size] & 0xFF;
            size += 1;
            while (blockSize != 0x00) {
                size += blockSize;
                blockSize = bytes[offset + size] & 0xFF;
                size += 1;
            }
            this.bytes = new byte[size];
            System.arraycopy(bytes, offset, this.bytes, 0, size);
        }
    }

    private class PlainTextExtension {
        public byte[] bytes;
        public int size;

        public PlainTextExtension(byte[] bytes, int offset) {
            int blockSize;
            // get size
            size = 0x0F;

            blockSize = bytes[offset + size] & 0xFF;
            size += 1;
            while (blockSize != 0x00) {
                size += blockSize;
                blockSize = bytes[offset + size] & 0xFF;
                size += 1;
            }
            this.bytes = new byte[size];
            System.arraycopy(bytes, offset, this.bytes, 0, size);
        }
    }

    public String toHex(int value, int length) {
        String hex = Integer.toHexString(value);
        hex = hex.toUpperCase();
        if (hex.length() < length) {
            while (hex.length() < length)
                hex = "0" + hex;
        } else if (hex.length() > length) {
            hex = hex.substring(hex.length() - length);
        }
        return hex;
    }

    public byte[] streamToBytes(InputStream stream) throws IOException,
            OutOfMemoryError {
        byte[] buff = new byte[1024];
        int read;
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        while ((read = stream.read(buff)) != -1) {
            bao.write(buff, 0, read);
        }
        try {
            stream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bao.toByteArray();
    }
}